/*-------------------------------------------------------------------------
 *
 * matview.c
 *	  物化视图支持
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/matview.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
#include "access/tableam.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/catalog.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/pg_am.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "commands/cluster.h"
#include "commands/matview.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
#include "miscadmin.h"
#include "parser/parse_relation.h"
#include "pgstat.h"
#include "rewrite/rewriteHandler.h"
#include "storage/lmgr.h"
#include "storage/smgr.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"


typedef struct
{
	DestReceiver pub;			/* 公共已知的函数指针 */
	Oid			transientoid;	/* 新堆的OID，用于存储 */
	/* 这些字段由transientrel_startup填充: */
	Relation	transientrel;	/* 写入的关系 */
	CommandId	output_cid;		/* 插入输出元组的 cmin */
	int			ti_options;		/* table_tuple_insert 性能选项 */
	BulkInsertState bistate;	/* 批量插入状态 */
} DR_transientrel;

static int	matview_maintenance_depth = 0;

static void fc_transientrel_startup(DestReceiver *fc_self, int fc_operation, TupleDesc fc_typeinfo);
static bool fc_transientrel_receive(TupleTableSlot *fc_slot, DestReceiver *fc_self);
static void fc_transientrel_shutdown(DestReceiver *fc_self);
static void fc_transientrel_destroy(DestReceiver *fc_self);
static uint64 fc_refresh_matview_datafill(DestReceiver *fc_dest, Query *fc_query,
									   const char *fc_queryString);
static char *fc_make_temptable_name_n(char *fc_tempname, int fc_n);
static void fc_refresh_by_match_merge(Oid fc_matviewOid, Oid fc_tempOid, Oid fc_relowner,
								   int fc_save_sec_context);
static void fc_refresh_by_heap_swap(Oid fc_matviewOid, Oid fc_OIDNewHeap, char fc_relpersistence);
static bool fc_is_usable_unique_index(Relation fc_indexRel);
static void fc_OpenMatViewIncrementalMaintenance(void);
static void fc_CloseMatViewIncrementalMaintenance(void);

/*
 * SetMatViewPopulatedState
 *		标记一个物化视图为已填充或未填充。
 *
 * 注意：调用者必须持有该关系的适当锁。
 */
void SetMatViewPopulatedState(Relation fc_relation, bool fc_newstate)
{
	Relation	fc_pgrel;
	HeapTuple	fc_tuple;

	Assert(fc_relation->rd_rel->relkind == RELKIND_MATVIEW);

	/*
	 * 更新关系的pg_class条目。关键副作用：其他后端
	 * （以及这个后端！）会收到SI消息以使它们重建relcache
	 * 条目。
	 */
	fc_pgrel = table_open(RelationRelationId, RowExclusiveLock);
	fc_tuple = SearchSysCacheCopy1(RELOID,
								ObjectIdGetDatum(RelationGetRelid(fc_relation)));
	if (!HeapTupleIsValid(fc_tuple))
		elog(ERROR, "cache lookup failed for relation %u",
			 RelationGetRelid(fc_relation));

	((Form_pg_class) GETSTRUCT(fc_tuple))->relispopulated = fc_newstate;

	CatalogTupleUpdate(fc_pgrel, &fc_tuple->t_self, fc_tuple);

	heap_freetuple(fc_tuple);
	table_close(fc_pgrel, RowExclusiveLock);

	/*
	 * 前进命令计数器，使更新的pg_class行在本地
	 * 可见。
	 */
	CommandCounterIncrement();
}

/*
 * ExecRefreshMatView -- 执行REFRESH MATERIALIZED VIEW命令
 *
 * 通过创建一个新表并交换新表和旧物化视图的relfilenodes来刷新
 * 物化视图，因此原始物化视图的OID得以保留。因此我们不会失去GRANT
 * 或对此物化视图的引用。
 *
 * 如果指定了WITH NO DATA，这实际上就像是TRUNCATE；
 * 否则，它就像是TRUNCATE后跟使用与物化视图关联的SELECT
 * 语句的INSERT。语句节点的skipData字段显示该子句是否被使用。
 *
 * 索引也通过REINDEX重建。由于我们实际上是批量加载
 * 新堆，因此在加载时逐步填充索引不如在之后创建索引更好。
 *
 * matview的“已填充”状态是根据内容是否
 * 反映物化视图查询的结果集来改变的。
 */
ObjectAddress ExecRefreshMatView(RefreshMatViewStmt *fc_stmt, const char *fc_queryString,
				   ParamListInfo fc_params, QueryCompletion *fc_qc)
{
	Oid			fc_matviewOid;
	Relation	fc_matviewRel;
	RewriteRule *fc_rule;
	List	   *fc_actions;
	Query	   *fc_dataQuery;
	Oid			fc_tableSpace;
	Oid			fc_relowner;
	Oid			fc_OIDNewHeap;
	DestReceiver *fc_dest;
	uint64		fc_processed = 0;
	bool		fc_concurrent;
	LOCKMODE	fc_lockmode;
	char		fc_relpersistence;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	int			fc_save_nestlevel;
	ObjectAddress fc_address;

	/* 确定所需锁的强度。 */
	fc_concurrent = fc_stmt->concurrent;
	fc_lockmode = fc_concurrent ? ExclusiveLock : AccessExclusiveLock;

	/*
	 * 获取锁直到事务结束。
	 */
	fc_matviewOid = RangeVarGetRelidExtended(fc_stmt->relation,
										  fc_lockmode, 0,
										  RangeVarCallbackOwnsTable, NULL);
	fc_matviewRel = table_open(fc_matviewOid, NoLock);
	fc_relowner = fc_matviewRel->rd_rel->relowner;

	/*
	 * 切换到所有者的用户ID，以便以该
	 * 用户身份运行任何函数。还需要锁定安全限制操作并安排
	 * 使GUC变量更改局部化到该命令。
	 */
	GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
	SetUserIdAndSecContext(fc_relowner,
						   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
	fc_save_nestlevel = NewGUCNestLevel();

	/* 确保它是一个物化视图。 */
	if (fc_matviewRel->rd_rel->relkind != RELKIND_MATVIEW)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("\"%s\" is not a materialized view",
						RelationGetRelationName(fc_matviewRel))));

	/* 检查如果未填充，则未指定CONCURRENTLY。 */
	if (fc_concurrent && !RelationIsPopulated(fc_matviewRel))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("CONCURRENTLY cannot be used when the materialized view is not populated")));

	/* 检查未指定冲突选项。 */
	if (fc_concurrent && fc_stmt->skipData)
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("%s and %s options cannot be used together",
						"CONCURRENTLY", "WITH NO DATA")));

	/*
	 * 检查刷新时一切是否正确。此时的问题
	 * 是内部错误，因此elog就足够了。
	 */
	if (fc_matviewRel->rd_rel->relhasrules == false ||
		fc_matviewRel->rd_rules->numLocks < 1)
		elog(ERROR,
			 "materialized view \"%s\" is missing rewrite information",
			 RelationGetRelationName(fc_matviewRel));

	if (fc_matviewRel->rd_rules->numLocks > 1)
		elog(ERROR,
			 "materialized view \"%s\" has too many rules",
			 RelationGetRelationName(fc_matviewRel));

	fc_rule = fc_matviewRel->rd_rules->rules[0];
	if (fc_rule->event != CMD_SELECT || !(fc_rule->isInstead))
		elog(ERROR,
			 "the rule for materialized view \"%s\" is not a SELECT INSTEAD OF rule",
			 RelationGetRelationName(fc_matviewRel));

	fc_actions = fc_rule->actions;
	if (list_length(fc_actions) != 1)
		elog(ERROR,
			 "the rule for materialized view \"%s\" is not a single action",
			 RelationGetRelationName(fc_matviewRel));

	/*
	 * 检查如果指定了CONCURRENTLY，物化视图的一个或多个
	 * 列上是否存在没有WHERE子句的唯一索引。
	 */
	if (fc_concurrent)
	{
		List	   *fc_indexoidlist = RelationGetIndexList(fc_matviewRel);
		ListCell   *fc_indexoidscan;
		bool		fc_hasUniqueIndex = false;

		foreach(fc_indexoidscan, fc_indexoidlist)
		{
			Oid			fc_indexoid = lfirst_oid(fc_indexoidscan);
			Relation	fc_indexRel;

			fc_indexRel = index_open(fc_indexoid, AccessShareLock);
			fc_hasUniqueIndex = fc_is_usable_unique_index(fc_indexRel);
			index_close(fc_indexRel, AccessShareLock);
			if (fc_hasUniqueIndex)
				break;
		}

		list_free(fc_indexoidlist);

		if (!fc_hasUniqueIndex)
			ereport(ERROR,
					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
					 errmsg("cannot refresh materialized view \"%s\" concurrently",
							quote_qualified_identifier(get_namespace_name(RelationGetNamespace(fc_matviewRel)),
													   RelationGetRelationName(fc_matviewRel))),
					 errhint("Create a unique index with no WHERE clause on one or more columns of the materialized view.")));
	}

	/*
	 * 存储的查询在MV定义时被重写，但
	 * 还没有被计划器更改。
	 */
	fc_dataQuery = linitial_node(Query, fc_actions);

	/*
	 * 检查当前事务中对关系的有效使用，例如
	 * 打开的扫描。
	 *
	 * 注意：我们依靠这一点来保护我们免受使用TABLE_INSERT_FROZEN
	 * 刷新数据的问题。
	 */
	CheckTableNotInUse(fc_matviewRel, "REFRESH MATERIALIZED VIEW");

	/*
	 * 暂时标记matview为已填充或未填充（如果后续
	 * 失败，则会回滚）。
	 */
	SetMatViewPopulatedState(fc_matviewRel, !fc_stmt->skipData);

	/* 并发刷新在临时表空间中构建新数据，并进行差异处理。 */
	if (fc_concurrent)
	{
		fc_tableSpace = GetDefaultTablespace(RELPERSISTENCE_TEMP, false);
		fc_relpersistence = RELPERSISTENCE_TEMP;
	}
	else
	{
		fc_tableSpace = fc_matviewRel->rd_rel->reltablespace;
		fc_relpersistence = fc_matviewRel->rd_rel->relpersistence;
	}

	/*
	 * 创建一个临时表以接收重新生成的数据。锁定
	 * 它以防止任何其他进程在提交前访问（到时它
	 * 将消失）。
	 */
	fc_OIDNewHeap = make_new_heap(fc_matviewOid, fc_tableSpace,
							   fc_matviewRel->rd_rel->relam,
							   fc_relpersistence, ExclusiveLock);
	LockRelationOid(fc_OIDNewHeap, AccessExclusiveLock);
	fc_dest = CreateTransientRelDestReceiver(fc_OIDNewHeap);

	/* 如果需要，生成数据。 */
	if (!fc_stmt->skipData)
		fc_processed = fc_refresh_matview_datafill(fc_dest, fc_dataQuery, fc_queryString);

	/* 使物化视图与新生成的数据匹配。 */
	if (fc_concurrent)
	{
		int			fc_old_depth = matview_maintenance_depth;

		PG_TRY();
		{
			fc_refresh_by_match_merge(fc_matviewOid, fc_OIDNewHeap, fc_relowner,
								   fc_save_sec_context);
		}
		PG_CATCH();
		{
			matview_maintenance_depth = fc_old_depth;
			PG_RE_THROW();
		}
		PG_END_TRY();
		Assert(matview_maintenance_depth == fc_old_depth);
	}
	else
	{
		fc_refresh_by_heap_swap(fc_matviewOid, fc_OIDNewHeap, fc_relpersistence);

		/*
		 * 将我们的活动通知累积统计系统：基本上，我们
		 * 截断了物化视图并插入了一些新数据。 （上面的
		 * 并发代码路径不需要担心这一点，因为
		 * 它发出的插入和删除会被更低级别的代码计数。）
		 */
		pgstat_count_truncate(fc_matviewRel);
		if (!fc_stmt->skipData)
			pgstat_count_heap_insert(fc_matviewRel, fc_processed);
	}

	table_close(fc_matviewRel, NoLock);

	/* 回滚任何 GUC 更改 */
	AtEOXact_GUC(false, fc_save_nestlevel);

	/* 恢复 userid 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	ObjectAddressSet(fc_address, RelationRelationId, fc_matviewOid);

	/*
	 * 保存行计数，以便 pg_stat_statements 可以跟踪
	 * REFRESH MATERIALIZED VIEW 命令处理的总行数。请注意，我们
	 * 仍然不在命令完成标记输出中显示行计数，
	 * 即 CMDTAG_REFRESH_MATERIALIZED_VIEW
	 * 命令标记的 display_rowcount 标志在 cmdtaglist.h 中保持为 false。
	 * 否则，完成标记输出的变化可能会破坏使用它的应用程序。
	 */
	if (fc_qc)
		SetQueryCompletion(fc_qc, CMDTAG_REFRESH_MATERIALIZED_VIEW, fc_processed);

	return fc_address;
}

/*
 * refresh_matview_datafill
 *
 * 执行给定查询，将结果行发送到“dest”（这将
 * 将它们插入目标物化视图）。
 *
 * 返回插入的行数。
 */
static uint64 fc_refresh_matview_datafill(DestReceiver *fc_dest, Query *fc_query,
						 const char *fc_queryString)
{
	List	   *fc_rewritten;
	PlannedStmt *fc_plan;
	QueryDesc  *fc_queryDesc;
	Query	   *fc_copied_query;
	uint64		fc_processed;

	/* 锁定并重写，使用副本来保留原始查询。 */
	fc_copied_query = copyObject(fc_query);
	AcquireRewriteLocks(fc_copied_query, true, false);
	fc_rewritten = QueryRewrite(fc_copied_query);

	/* SELECT 应该永远不会重写为多于或少于一个 SELECT 查询 */
	if (list_length(fc_rewritten) != 1)
		elog(ERROR, "unexpected rewrite result for REFRESH MATERIALIZED VIEW");
	fc_query = (Query *) linitial(fc_rewritten);

	/* 检查用户请求的中止。 */
	CHECK_FOR_INTERRUPTS();

	/* 计划将为刷新生成数据的查询。 */
	fc_plan = pg_plan_query(fc_query, fc_queryString, CURSOR_OPT_PARALLEL_OK, NULL);

	/*
	 * 使用带有更新的命令 ID 的快照以确保该查询看到
	 * 任何先前执行查询的结果。 （这只有在
	 * 规划者执行了一个声称是稳定的函数并改变了
	 * 数据库内容时才重要，但我们还是这样做以确保安全。）
	 */
	PushCopiedSnapshot(GetActiveSnapshot());
	UpdateActiveSnapshotCommandId();

	/* 创建一个 QueryDesc，将输出重定向到我们的元组接收器 */
	fc_queryDesc = CreateQueryDesc(fc_plan, fc_queryString,
								GetActiveSnapshot(), InvalidSnapshot,
								fc_dest, NULL, NULL, 0);

	/* 调用 ExecutorStart 准备执行计划 */
	ExecutorStart(fc_queryDesc, 0);

	/* 运行计划 */
	ExecutorRun(fc_queryDesc, ForwardScanDirection, 0L, true);

	fc_processed = fc_queryDesc->estate->es_processed;

	/* 并进行清理 */
	ExecutorFinish(fc_queryDesc);
	ExecutorEnd(fc_queryDesc);

	FreeQueryDesc(fc_queryDesc);

	PopActiveSnapshot();

	return fc_processed;
}

DestReceiver * CreateTransientRelDestReceiver(Oid fc_transientoid)
{
	DR_transientrel *fc_self = (DR_transientrel *) palloc0(sizeof(DR_transientrel));

	fc_self->pub.receiveSlot = fc_transientrel_receive;
	fc_self->pub.rStartup = fc_transientrel_startup;
	fc_self->pub.rShutdown = fc_transientrel_shutdown;
	fc_self->pub.rDestroy = fc_transientrel_destroy;
	fc_self->pub.mydest = DestTransientRel;
	fc_self->transientoid = fc_transientoid;

	return (DestReceiver *) fc_self;
}

/*
 * transientrel_startup --- 执行器启动
 */
static void fc_transientrel_startup(DestReceiver *fc_self, int fc_operation, TupleDesc fc_typeinfo)
{
	DR_transientrel *fc_myState = (DR_transientrel *) fc_self;
	Relation	fc_transientrel;

	fc_transientrel = table_open(fc_myState->transientoid, NoLock);

	/*
	 * 填充 myState 的私有字段，以供后续例程使用
	 */
	fc_myState->transientrel = fc_transientrel;
	fc_myState->output_cid = GetCurrentCommandId(true);
	fc_myState->ti_options = TABLE_INSERT_SKIP_FSM | TABLE_INSERT_FROZEN;
	fc_myState->bistate = GetBulkInsertState();

	/*
	 * 有效的 smgr_targblock 意味着已经有写入操作发生在该关系上。这可能是无害的，但此函数并没有为此做计划。
	 */
	Assert(RelationGetTargetBlock(fc_transientrel) == InvalidBlockNumber);
}

/*
 * transientrel_receive --- 接收一个元组
 */
static bool fc_transientrel_receive(TupleTableSlot *fc_slot, DestReceiver *fc_self)
{
	DR_transientrel *fc_myState = (DR_transientrel *) fc_self;

	/*
	 * 注意，输入槽的类型可能不是目标
	 * 关系的类型。这被 table_tuple_insert() 支持，但效率略低于
	 * 使用正确槽插入 - 但替代方案是复制到正确类型的槽，这也不便宜。
	 * 这也不允许访问按 AM 数据（例如元组的 xmin），但由于我们在这里不执行...
	 */

	table_tuple_insert(fc_myState->transientrel,
					   fc_slot,
					   fc_myState->output_cid,
					   fc_myState->ti_options,
					   fc_myState->bistate);

	/* 我们知道这是一个新创建的关系，因此没有索引 */

	return true;
}

/*
 * transientrel_shutdown --- 执行器结束
 */
static void fc_transientrel_shutdown(DestReceiver *fc_self)
{
	DR_transientrel *fc_myState = (DR_transientrel *) fc_self;

	FreeBulkInsertState(fc_myState->bistate);

	table_finish_bulk_insert(fc_myState->transientrel, fc_myState->ti_options);

	/* 关闭 transientrel，但在提交前保持锁定 */
	table_close(fc_myState->transientrel, NoLock);
	fc_myState->transientrel = NULL;
}

/*
 * transientrel_destroy --- 释放 DestReceiver 对象
 */
static void fc_transientrel_destroy(DestReceiver *fc_self)
{
	pfree(fc_self);
}


/*
 * 给定一个合格的临时表名，在其后附加一个下划线和给定的整数，以基于旧表名生成一个新表名。
 * 结果是一个 palloc 的字符串。
 *
 * 按照代码的编写方式，如果给定的名称是 "FOO"."BAR"，这将无法生成有效的 SQL 名称。
 * 目前，输入的表名部分将永远不会被双引号包围，因为它的形式为 "pg_temp_NNN"，参见 make_new_heap()。
 * 但将来我们可能需要更加努力。
 */
static char * fc_make_temptable_name_n(char *fc_tempname, int fc_n)
{
	StringInfoData fc_namebuf;

	initStringInfo(&fc_namebuf);
	appendStringInfoString(&fc_namebuf, fc_tempname);
	appendStringInfo(&fc_namebuf, "_%d", fc_n);
	return fc_namebuf.data;
}

/*
 * refresh_by_match_merge
 *
 * 以事务语义刷新物化视图，同时允许并发读取。
 *
 * 此函数在临时表中创建新版本的数据后被调用。它对旧版本的数据执行完全外连接，生成“差异”结果。
 * 如果在旧版本或新版本中有任何重复行（即每一列在两行之间都被视为相等），则该连接将无法工作。
 * 它在至少有一个 NULL 值且所有非 NULL 列相等的行面前可以正确工作。
 * NULL 在相等测试和 UNIQUE 索引上的行为在这里显得相当方便；我们需要的测试与默认行为是一致的。
 * 如果物化视图上至少有一个 UNIQUE 索引，我们就得到了所需的保证。
 *
 * 用于保存差异结果的临时表仅包含旧记录的 TID（如果匹配）和新表的 ROW，作为一种复杂记录类型的单列（如果匹配）。
 *
 * 一旦我们有了差异表，就对物化视图执行基于集合的 DELETE 和 INSERT 操作，并丢弃这两个临时表。
 *
 * 从新数据生成到应用差异的一切操作都在 ExclusiveLock 的保护下进行，因为似乎我们希望禁止不仅是并发的 REFRESH 操作，还有增量维护。
 * 对于此命令正在更新或删除的行，允许 SELECT FOR UPDATE 或 SELECT FOR SHARE 似乎也不合理或安全。
 */
static void fc_refresh_by_match_merge(Oid fc_matviewOid, Oid fc_tempOid, Oid fc_relowner,
					   int fc_save_sec_context)
{
	StringInfoData fc_querybuf;
	Relation	fc_matviewRel;
	Relation	fc_tempRel;
	char	   *fc_matviewname;
	char	   *fc_tempname;
	char	   *fc_diffname;
	TupleDesc	fc_tupdesc;
	bool		fc_foundUniqueIndex;
	List	   *fc_indexoidlist;
	ListCell   *fc_indexoidscan;
	int16		fc_relnatts;
	Oid		   *fc_opUsedForQual;

	initStringInfo(&fc_querybuf);
	fc_matviewRel = table_open(fc_matviewOid, NoLock);
	fc_matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(fc_matviewRel)),
											 RelationGetRelationName(fc_matviewRel));
	fc_tempRel = table_open(fc_tempOid, NoLock);
	fc_tempname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(fc_tempRel)),
										  RelationGetRelationName(fc_tempRel));
	fc_diffname = fc_make_temptable_name_n(fc_tempname, 2);

	fc_relnatts = RelationGetNumberOfAttributes(fc_matviewRel);

	/* 打开 SPI 上下文。 */
	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/* 分析包含新内容的临时表。 */
	appendStringInfo(&fc_querybuf, "ANALYZE %s", fc_tempname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_UTILITY)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	/*
	 * 在可以依赖“差异”结果之前，我们需要确保新数据集中不存在没有 NULL 的重复行。
	 * 以允许显示找到的第一个重复行的方式进行检查。即使在通过此测试后，物化视图上的唯一索引也可能会发现重复键问题。
	 *
	 * 注意：在这里和下面，我们使用 "tablename.*::tablerowtype" 作为一种 hack，以防止在 SELECT 列表中将 ".*" 扩展为多个列。
	 * 请比较 ruleutils.c 中的 get_variable()。
	 */
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf,
					 "SELECT newdata.*::%s FROM %s newdata "
					 "WHERE newdata.* IS NOT NULL AND EXISTS "
					 "(SELECT 1 FROM %s newdata2 WHERE newdata2.* IS NOT NULL "
					 "AND newdata2.* OPERATOR(pg_catalog.*=) newdata.* "
					 "AND newdata2.ctid OPERATOR(pg_catalog.<>) "
					 "newdata.ctid)",
					 fc_tempname, fc_tempname, fc_tempname);
	if (SPI_execute(fc_querybuf.data, false, 1) != SPI_OK_SELECT)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);
	if (SPI_processed > 0)
	{
		/*
		 * 注意，这个 ereport() 正在将数据返回给用户。通常，
		 * 我们希望确保用户已获准访问这些数据。然而，REFRESH MAT VIEW 只能由
		 * mat view 的所有者（或超级用户）运行，因此没有必要检查
		 * 对 mat view 中数据的访问权限。
		 */
		ereport(ERROR,
				(errcode(ERRCODE_CARDINALITY_VIOLATION),
				 errmsg("new data for materialized view \"%s\" contains duplicate rows without any null columns",
						RelationGetRelationName(fc_matviewRel)),
				 errdetail("Row: %s",
						   SPI_getvalue(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1))));
	}

	/*
	 * 创建临时的 "diff" 表。
	 *
	 * 暂时退出 SECURITY_RESTRICTED_OPERATION 上下文，
	 * 因为在 SRO 上下文中不能创建临时表。为了额外的
	 * 小心，只有在切换回 SRO 上下文后才添加复合类型列。
	 */
	SetUserIdAndSecContext(fc_relowner,
						   fc_save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf,
					 "CREATE TEMP TABLE %s (tid pg_catalog.tid)",
					 fc_diffname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_UTILITY)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);
	SetUserIdAndSecContext(fc_relowner,
						   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf,
					 "ALTER TABLE %s ADD COLUMN newdata %s",
					 fc_diffname, fc_tempname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_UTILITY)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	/* 开始构建填充 diff 表的查询。 */
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf,
					 "INSERT INTO %s "
					 "SELECT mv.ctid AS tid, newdata.*::%s AS newdata "
					 "FROM %s mv FULL JOIN %s newdata ON (",
					 fc_diffname, fc_tempname, fc_matviewname, fc_tempname);

	/*
	 * 从 relcache 获取表的索引 OID 列表，并在 pg_index 系统缓存中查找
	 * 每一个。我们将测试所有唯一索引中仅引用列的所有
	 * 列的相等性，并包含所有行。
	 */
	fc_tupdesc = fc_matviewRel->rd_att;
	fc_opUsedForQual = (Oid *) palloc0(sizeof(Oid) * fc_relnatts);
	fc_foundUniqueIndex = false;

	fc_indexoidlist = RelationGetIndexList(fc_matviewRel);

	foreach(fc_indexoidscan, fc_indexoidlist)
	{
		Oid			fc_indexoid = lfirst_oid(fc_indexoidscan);
		Relation	fc_indexRel;

		fc_indexRel = index_open(fc_indexoid, RowExclusiveLock);
		if (fc_is_usable_unique_index(fc_indexRel))
		{
			Form_pg_index fc_indexStruct = fc_indexRel->rd_index;
			int			fc_indnkeyatts = fc_indexStruct->indnkeyatts;
			oidvector  *fc_indclass;
			Datum		fc_indclassDatum;
			bool		fc_isnull;
			int			fc_i;

			/* 必须以困难的方式获取 indclass。 */
			fc_indclassDatum = SysCacheGetAttr(INDEXRELID,
											fc_indexRel->rd_indextuple,
											Anum_pg_index_indclass,
											&fc_isnull);
			Assert(!fc_isnull);
			fc_indclass = (oidvector *) DatumGetPointer(fc_indclassDatum);

			/* 为该索引的所有列添加 qual。 */
			for (fc_i = 0; fc_i < fc_indnkeyatts; fc_i++)
			{
				int			fc_attnum = fc_indexStruct->indkey.values[fc_i];
				Oid			fc_opclass = fc_indclass->values[fc_i];
				Form_pg_attribute fc_attr = TupleDescAttr(fc_tupdesc, fc_attnum - 1);
				Oid			fc_attrtype = fc_attr->atttypid;
				HeapTuple	fc_cla_ht;
				Form_pg_opclass fc_cla_tup;
				Oid			fc_opfamily;
				Oid			fc_opcintype;
				Oid			fc_op;
				const char *fc_leftop;
				const char *fc_rightop;

				/*
				 * 确定与该索引列关联的相等操作符。首先，我们需要查找列的 opclass。
				 */
				fc_cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(fc_opclass));
				if (!HeapTupleIsValid(fc_cla_ht))
					elog(ERROR, "cache lookup failed for opclass %u", fc_opclass);
				fc_cla_tup = (Form_pg_opclass) GETSTRUCT(fc_cla_ht);
				Assert(fc_cla_tup->opcmethod == BTREE_AM_OID);
				fc_opfamily = fc_cla_tup->opcfamily;
				fc_opcintype = fc_cla_tup->opcintype;
				ReleaseSysCache(fc_cla_ht);

				fc_op = get_opfamily_member(fc_opfamily, fc_opcintype, fc_opcintype,
										 BTEqualStrategyNumber);
				if (!OidIsValid(fc_op))
					elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
						 BTEqualStrategyNumber, fc_opcintype, fc_opcintype, fc_opfamily);

				/*
				 * 如果我们在多个索引中找到具有相同相等语义的相同列，
				 * 我们只需要发出一次相等子句。
				 *
				 * 由于我们只记住最后一个相等操作符，这段代码可能会由于
				 * 存在多个不同的 opclass 的多个索引而被误导，导致发出重复子句……
				 * 但这太不可能发生，因此似乎没有必要花额外的代码来避免。
				 */
				if (fc_opUsedForQual[fc_attnum - 1] == fc_op)
					continue;
				fc_opUsedForQual[fc_attnum - 1] = fc_op;

				/*
				 * 实际上添加 qual，与其他任何条件一起 AND。
				 */
				if (fc_foundUniqueIndex)
					appendStringInfoString(&fc_querybuf, " AND ");

				fc_leftop = quote_qualified_identifier("newdata",
													NameStr(fc_attr->attname));
				fc_rightop = quote_qualified_identifier("mv",
													 NameStr(fc_attr->attname));

				generate_operator_clause(&fc_querybuf,
										 fc_leftop, fc_attrtype,
										 fc_op,
										 fc_rightop, fc_attrtype);

				fc_foundUniqueIndex = true;
			}
		}

		/* 保持锁定，因为我们即将运行需要它们的 DML。 */
		index_close(fc_indexRel, NoLock);
	}

	list_free(fc_indexoidlist);

	/*
	 * matview 上必须至少有一个可用的唯一索引。
	 *
	 * ExecRefreshMatView() 在对 matview 获取独占锁后进行检查。
	 * 因此，在这里保证至少存在一个唯一索引，因为锁仍然被保持着。
	 * （一个已知的例外情况是，如果在刷新 matview 的过程中调用的
	 * 函数删除了该索引。这是一件相当愚蠢的事情。）
	 */
	if (!fc_foundUniqueIndex)
		elog(ERROR, "could not find suitable unique index on materialized view");

	appendStringInfoString(&fc_querybuf,
						   " AND newdata.* OPERATOR(pg_catalog.*=) mv.*) "
						   "WHERE newdata.* IS NULL OR mv.* IS NULL "
						   "ORDER BY tid");

	/* 填充临时的 "diff" 表。 */
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_INSERT)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	/*
	 * 我们不再需要 "full-data" 临时表中的数据，但我们
	 * 必须保留它，因为它的类型被 diff 表引用。
	 */

	/* 分析 diff 表。 */
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf, "ANALYZE %s", fc_diffname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_UTILITY)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	fc_OpenMatViewIncrementalMaintenance();

	/* 删除必须在插入之前进行；先执行删除。 */
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf,
					 "DELETE FROM %s mv WHERE ctid OPERATOR(pg_catalog.=) ANY "
					 "(SELECT diff.tid FROM %s diff "
					 "WHERE diff.tid IS NOT NULL "
					 "AND diff.newdata IS NULL)",
					 fc_matviewname, fc_diffname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_DELETE)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	/* 插入最后执行。 */
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf,
					 "INSERT INTO %s SELECT (diff.newdata).* "
					 "FROM %s diff WHERE tid IS NULL",
					 fc_matviewname, fc_diffname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_INSERT)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	/* 我们完成了对物化视图的维护。 */
	fc_CloseMatViewIncrementalMaintenance();
	table_close(fc_tempRel, NoLock);
	table_close(fc_matviewRel, NoLock);

	/* 清理临时表。 */
	resetStringInfo(&fc_querybuf);
	appendStringInfo(&fc_querybuf, "DROP TABLE %s, %s", fc_diffname, fc_tempname);
	if (SPI_exec(fc_querybuf.data, 0) != SPI_OK_UTILITY)
		elog(ERROR, "SPI_exec failed: %s", fc_querybuf.data);

	/* 关闭 SPI 上下文。 */
	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");
}

/*
 * 交换目标表和临时表的物理文件，然后重建
 * 目标的索引，并丢弃临时表。安全上下文
 * 交换由被调用的函数处理，因此在这里不需要。
 */
static void fc_refresh_by_heap_swap(Oid fc_matviewOid, Oid fc_OIDNewHeap, char fc_relpersistence)
{
	finish_heap_swap(fc_matviewOid, fc_OIDNewHeap, false, false, true, true,
					 RecentXmin, ReadNextMultiXactId(), fc_relpersistence);
}

/*
 * 检查指定的索引是否可用于匹配合并。
 */
static bool fc_is_usable_unique_index(Relation fc_indexRel)
{
	Form_pg_index fc_indexStruct = fc_indexRel->rd_index;

	/*
	 * 必须是唯一的、有效的、即时的、非部分的，并且定义在
	 * 普通用户列上（而不是表达式）。我们还要求它是一个
	 * btree。即使我们有其他类型的唯一索引，我们也不知道如何
	 * 识别相应的相等操作符，也无法确保规划器能使用非-btree
	 * 操作符实现所需的FULL JOIN。
	 */
	if (fc_indexStruct->indisunique &&
		fc_indexStruct->indimmediate &&
		fc_indexRel->rd_rel->relam == BTREE_AM_OID &&
		fc_indexStruct->indisvalid &&
		RelationGetIndexPredicate(fc_indexRel) == NIL &&
		fc_indexStruct->indnatts > 0)
	{
		/*
		 * 个别遍历索引列的目的是拒绝索引表达式和系统列。目前，
		 * 物化视图不能拥有OID列，因此无法在系统列上创建索引；
		 * 但也许有一天这不再成立，所以让我们保持谨慎。
		 */
		int			fc_numatts = fc_indexStruct->indnatts;
		int			fc_i;

		for (fc_i = 0; fc_i < fc_numatts; fc_i++)
		{
			int			fc_attnum = fc_indexStruct->indkey.values[fc_i];

			if (fc_attnum <= 0)
				return false;
		}
		return true;
	}
	return false;
}


/*
 * 这应该用于测试后端是否处于允许DML语句修改物化视图的上下文。
 * 我们只希望允许内置代码根据物化视图定义驱动，而不是任意用户提供的代码。
 *
 * 虽然函数名称反映了它们的主要预期用途是增量维护物化视图
 * （以响应引用关系中数据的变化），但它们最初用于允许REFRESH
 * 而不阻塞并发读取。
 */
bool MatViewIncrementalMaintenanceIsEnabled(void)
{
	return matview_maintenance_depth > 0;
}

static void fc_OpenMatViewIncrementalMaintenance(void)
{
	matview_maintenance_depth++;
}

static void fc_CloseMatViewIncrementalMaintenance(void)
{
	matview_maintenance_depth--;
	Assert(matview_maintenance_depth >= 0);
}
